
import os
import ConfigParser
import codecs
import types

import ab_utils as utils

from ab_ids import *

CFG_PARAMETER_PLX_STATR = 0x100
RUNTIME_PLUGIN_SECTION = 'RT_Plugins'

_CFG_PARA_TEMPLATE = {
    'App Settings': {
        'name': 'App Settings',
        'section': 'Application',
        'options': {
            CfgParaAppRestart: {
                'name': 'Restart',
                'attr': 'bool,invisible'},
            CfgParaAppRestartInd: {
                'name': 'RestartInd',
                'attr': 'bool,invisible'},
            CfgParaDeveloperMethod: {
                'name': 'DeveloperMethod',
                'attr': 'bool,restart',
                'default': True}}},
    'UI Settings': {
        'name': 'UI Settings',
        'section': 'View',
        'options': {
            CfgParaAnaFileTop: {
                'name': 'Top',
                'attr': 'int',
                'default': -1},
            CfgParaAnaFileLeft: {
                'name': 'Left',
                'attr': 'int',
                'default': -1},
            CfgParaAnaFileWidth: {
                'name': 'Width',
                'attr': 'int',
                'default': 800},
            CfgParaAnaFileHeight: {
                'name': 'Height',
                'attr': 'int',
                'default': 600},
            CfgParaDefaultRecNum: {
                'name': 'MaxRecentNumber',
                'attr': 'int',
                'default': 5},
            CfgParaFileRecentOpened: {
                'name': 'RecentFile',
                'attr': 'str,range,file'},
            CfgParaEnableTextMode: {
                'name': 'EnableTextMode',
                'attr': 'bool',
                'default': True},
            CfgParaLineLength: {
                'name': 'LengthOfLine',
                'attr': 'int',
                'default': 16},
            CfgParaEnableAutoLine: {
                'name': 'EnableAutoLine',
                'attr': 'bool'},
            CfgParaTreeMatchEnable: {
                'name': 'TreeMatchEnable',
                'attr': 'bool',
                'default': True},
            CfgParaTreeAllMatches: {
                'name': 'TreeAllMatches',
                'attr': 'bool'},
            CfgParaTreeIgnoredLevel: {
                'name': 'TreeIgnoredLevel',
                'attr': 'int',
                'default': 10},
            CfgParaTreeAutoFocus: {
                'name': 'TreeAutoFocuse',
                'attr': 'bool'},
            CfgParaTreeMatchMode: {
                'name': 'TreeMatchMode',
                'attr': 'bool'},
            CfgParaDisplayPicture: {
                'name': 'DisplayPicture',
                'attr': 'bool',
                'default': True},
            CfgParaColorFontHigh: {
                'name': 'HlFontColor',
                'attr': 'int,color',
                'default': 0xffffff},
            CfgParaColorBackHigh: {
                'name': 'HlBackColor',
                'attr': 'int,color',
                'default': 0x000000},
            CfgParaColorFontLabel: {
                'name': 'LblFontColor',
                'attr': 'int,color',
                'default': 0xffffff},
            CfgParaColorBackLabel: {
                'name': 'LblBackColor',
                'attr': 'int,color',
                'default': 0xff},
            CfgParaColorSeparator: {
                'name': 'SeparatorColor',
                'attr': 'int,color',
                'default': 0xbbbbbb},
            CfgParaTreeColor: {
                'name': 'TreeColor',
                'attr': 'int,color',
                'default': 0x0000}}},
    'Logging Settings': {
        'name': 'Logging Settings',
        'section': 'Logs',
        'options': {
            CfgParaVerbose: {
                'name': 'Verbose',
                'attr': 'int,enum',
                'enum': (('CRITICAL', 'ERROR', 'WARNING',
                          'INFO', 'DEBUG', 'NOTSET'),),
                'default': 4},
            CfgParaFileLogName: {
                'name': 'LogFile',
                'attr': 'str,file,restart',
                'default': os.path.join('%APP_DATA%', 'logfile.txt')}}},
    'Object Settings': {
        'name': 'Object Settings',
        'section': 'Object',
        'attr': 'object,invisible',
        'options': {
            # object parameters
            CfgParaLogObject: {
                'name': 'Log'},
            CfgParaEntityObject: {
                'name': 'Entire'},
            CfgParaPlxObject: {
                'name': 'Plugin'},
            CfgParaHelpObject: {
                'name': 'Help'},
            CfgParaEscapeObject: {
                'name': 'Escape'},
            CfgParaConfigObject: {
                'name': 'Config'},
            CfgParaResourceObject: {
                'name': 'Resource'},
            CfgParaEnvObject: {
                'name': 'Environ'},
            CfgParaCwd: {
                'name': 'CWD',
                'attr': 'str,dir'},
            CfgParaConsole: {
                'name': 'Console',
                'attr': 'bool'},
            CfgParaPlatform: {
                'name': 'Platform'},
            CfgParaDisplayFont: {
                'name': 'Font',
                'attr': 'font'}}},
    'Plugin Settings': {
        'name': 'Plugin Settings',
        'section': 'Plugins',
        'options': {
            CfgParaReloadIndicator: {
                'name': 'Reload',
                'attr': 'bool',
                'default': True},
            CfgParaReloadBackground: {
                'name': 'ReloadOnBackground',
                'attr': 'bool'},
            CfgParaReloadSuppress: {
                'name': 'ReloadSuppressed',
                'attr': 'bool,invisible'},
            CfgParaIgoreIntfError: {
                'name': 'IgnoreIntfError',
                'attr': 'bool',
                'default': True},
            CfgParaIgnorePlxConfig: {
                'name': 'IgnoreConfiguration',
                'attr': 'bool'},
            CfgParaExtraPlx: {
                'name': 'ExtraList',
                'attr': 'str,dir,range,restart'},
            CfgParaPlxEnableAll: {
                'name': 'EnableAll',
                'attr': 'bool,restart',
                'default': True},
            CfgParaPlxEnabledList: {
                'name': 'EnabledList',
                'attr': 'str,range,restart'},
            CfgParaPlxPath: {
                'name': 'Path',
                'attr': 'str,dir,restart',
                'default': os.path.join('%APP_PATH%', 'plugins')},
            CfgParaPlxEnabledCategory: {
                'name': 'EnabledCategory',
                'attr': 'bool,range,restart'},
            CfgParaBlockCacheSize: {
                'name': 'BlockSize',
                'attr': 'int',
                'default': 0x100000},
            CfgParaTreeUseSimpleAsDefault: {
                'name': 'TreeSimpleAsDefault',
                'attr': 'bool'},
            CfgParaTreeDispFormat: {
                'name': 'TreeFormat',
                'attr': 'str',
                'default': '%s: %c [Offset: %o, Size: %l]'},
            CfgParaTreeSimpleDispFormat: {
                'name': 'TreeSimpleFormat',
                'attr': 'str',
                'default': '%s: %c'},
            CfgParaTreeSimplestDispFormat: {
                'name': 'TreeSimplestFormat',
                'attr': 'str',
                'default': '%s'}}},
    'Plugin (Runtime) Settings': {
        'name': 'Plugin (Runtime) Settings',
        'section': RUNTIME_PLUGIN_SECTION,
        'attr': 'string,range',
        'options': {}}
}


class Item:
    def __init__(self, name, attr, value=None, key=None,
                 section=None, raw=False, removeSpace=False):
        self.orig = name
        self.name = name.lower()
        if removeSpace:
            self.name = name.lower().replace(' ', '')

        if isinstance(attr, (str, unicode)):
            self.attr = attr.split(',')
        else:
            self.attr = attr or list()

        self.value = value
        self.key = key
        self.section = (section or '').split('/')
        self.raw = raw
        self.persist = True
        self.tempValue = None

    @staticmethod
    def newInstance(section, option, value=None, key=None,
                    raw=False, removeSpace=False):
        def _dup(str_, delimeter):
            ret, str_ = str_, None
            while ret != str_:
                ret, str_ = ret.replace(delimeter + delimeter, delimeter), ret

            if ret.startswith(delimeter):
                ret = ret[len(delimeter):]
            if ret.endswith(delimeter):
                ret = ret[:len(ret) - len(delimeter)]

            return ret

        sect = section.get('section', '')
        name = _dup('/'.join([sect, option.get('name', '')]), '/')
        attr = _dup(','.join([section.get('attr', ''),
                    option.get('attr', '')]), ',').split(',')

        if value is None:
            value = Item.default(option, attr)

        return Item(name, attr, value, key=key, section=sect,
                    raw=raw, removeSpace=removeSpace)

    @staticmethod
    def default(option, attr):
        if 'range' in attr:
            ret = option.get('default', dict())
        elif 'int' in attr:
            ret = option.get('default', 0)
        elif 'bool' in attr:
            ret = option.get('default', False)
        elif 'float' in attr:
            ret = option.get('default', 0.0)
        elif 'str' in attr:
            ret = option.get('default', '')
        else:
            ret = option.get('default', None)

        return ret

    @staticmethod
    def ensureValue(attr, value):
        def _getEffectiveAttribute(attr):
            _TYPE_STR = {
                'int': 'int', 'bool': 'bool', 'float': 'float',
                'str': 'str', 'file': 'str', 'dir': 'str'}

            for k, v in _TYPE_STR.items():
                if k in attr:
                    return v

            return None

        tp = _getEffectiveAttribute(attr)
        if tp is None:
            return value

        if tp == 'bool':
            # bool("False") == True, use bool(False)
            fmts = "%s(%s)"
        else:
            fmts = "%s('%s')"

        if isinstance(value, (list, tuple)):
            alls = list()
            for v in value:
                try:
                    alls.append(v if tp == 'str' else eval(fmts % (tp, v)))
                except (TypeError, ValueError):
                    pass

            return alls
        elif isinstance(value, dict):
            alls = dict()
            for k, v in value.items():
                try:
                    alls[k] = v if tp == 'str' else eval(fmts % (tp, v))
                except (TypeError, ValueError):
                    pass

            return alls
        else:
            try:
                return value if tp == 'str' else eval(fmts % (tp, value))
            except (TypeError, ValueError):
                pass

        return value

    def remove(self, idx=None):
        if 'range' in self.attr and idx in self.value:
            del self.value[idx]

    def set(self, value, idx=None, raw=False, persist=True):
        self.raw = raw

        # ensure the value type
        value = Item.ensureValue(self.attr, value)
        if not persist:
            self.persist = False
            self.tempValue = self.value

        if 'range' in self.attr:
            if idx is not None:
                self.value[idx] = value
            elif isinstance(value, dict):
                self.value = value
            elif isinstance(value, (list, tuple)):
                self.value = dict()
                for ii, val in enumerate(value):
                    self.value[ii] = val
        else:
            self.value = value

    def get(self, idx=None):
        ret = self.value
        if ret is None:
            if 'range' in self.attr:
                ret = dict()
            elif 'int' in self.attr:
                ret = 0
            elif 'bool' in self.attr:
                ret = False
            elif 'float' in self.attr:
                ret = 0.0
            elif 'str' in self.attr:
                ret = ''

        if 'range' in self.attr:
            if isinstance(ret, dict) and idx is not None:
                return ret.get(idx)

        return ret


class TemplateHandler:
    def __init__(self, template, fixChar=False):
        self.items = dict()

        if template is not None:
            self.items = self.doConvertToItems(template, fixChar)

    def doConvertToItems(self, template, fixChar):
        citems = dict()

        if template is not None:
            for section in template.values():
                options = section.get('options', dict())
                for opt, option in options.items():
                    citems[opt] = Item.newInstance(
                        section, option, key=opt, removeSpace=fixChar)

        return citems

    def set(self, key, value, idx=None, raw=False, escape=False, persist=True):
        if escape:
            def _unescape(env, str_):
                if isinstance(str_, (str, unicode)):
                    return env.unescape(str_)

                return str_

            value_ = value
            env = self.get(CfgParaEnvObject)
            if isinstance(value_, (list, tuple)):
                value = list()
                for ii in value_:
                    value.append(_unescape(env, ii))
            elif isinstance(value_, dict):
                value = dict()
                for ii in value_:
                    value[ii] = _unescape(env, value_[ii])
            elif isinstance(value_, (str, unicode)):
                value = _unescape(env, value_)

        item = self.getItem(key)
        if item is not None:
            item.set(value, idx, raw=raw, persist=persist)

    def get(self, key, idx=None, escape=False):
        ret, item = None, None

        if key in self.items:
            item = self.items[key]
            ret = self.items[key].get(idx)
        else:
            for item in self.items.values():
                if item.name == key:
                    ret = item.get(idx)
                    break

        if escape and ret is not None and 'str' in item.attr:
            env = self.get(CfgParaEnvObject)

            ret_ = ret
            if isinstance(ret_, (list, tuple)):
                ret = list()
                for ii in ret_:
                    ret.append(env.escape(ii))
            elif isinstance(ret_, dict):
                ret = dict()
                for ii, val in ret_.items():
                    ret[ii] = env.escape(val)
            elif isinstance(ret_, (str, unicode)):
                ret = env.escape(ret_)

        return ret

    def getItem(self, key):
        if key in self.items:
            return self.items[key]
        else:
            for item in self.items.values():
                if item.name == key:
                    return item

        return None

    def getItems(self):
        return self.items


class Reader(TemplateHandler):
    def __init__(self, filename, template=None):

        def _read(config, section, option, attr=''):
            ret = None
            if 'int' in attr:
                ret = config.getint(section, option)
            elif 'bool' in attr:
                ret = config.getboolean(section, option)
            elif 'float' in attr:
                ret = config.getfloat(section, option)
            else:
                ret = config.get(section, option)

            return ret

        TemplateHandler.__init__(self, template)

        config = ConfigParser.ConfigParser()
        if os.path.exists(filename):
            config.readfp(utils.openfile(filename, 'r', utf8=True))

        for section in config.sections():
            for opt in config.options(section):
                name = '/'.join((section, opt)).lower()

                item = self.getItem(name)
                if item is not None:
                    item.value = _read(config, section, opt, item.attr)
                else:
                    for item in self.items.values():
                        if name.startswith(item.name + '_') \
                                and 'range' in item.attr:
                            value = _read(config, section, opt, item.attr)
                            postfix = name[len(item.name) + 1:]
                            try:
                                item.set(value, int(postfix))
                            except ValueError:
                                item.set(value, postfix)

                            break
                    else:
                        # use the option name as the key directly
                        self.items[name] = Item(
                            name, 'str', config.get(section, opt),
                            section=section, key=name, raw=True)

    def readKey(self, key, idx=None, escape=True):
        return self.get(key, idx, escape=escape)


class Writer(TemplateHandler):
    def __init__(self, filename, template):
        TemplateHandler.__init__(self, None)

        self.filename = filename
        self.template = template

    def insertOption(self, sect, options):
        for section in self.template.values():
            if section.get('section') == sect:
                for opt, option in options.items():
                    section['options'][opt] = option

                return True

        return False

    def writeKey(self, key, value, idx=None, escape=False, persist=True):
        item = self.getItem(key)
        if item is not None:
            if idx is None:
                old = self.get(key)
                self.set(key, value, persist=persist)
                changed = old != self.get(key)
            else:
                old = self.get(key, idx)
                self.set(key, value, idx, persist=persist)
                changed = old != self.get(key, idx)

            # set the restart if the key needs a restart
            if changed and 'restart' in item.attr:
                self.set(CfgParaAppRestartInd, True)
        else:
            self.set(key, value, raw=True, persist=persist)

    def removeKey(self, key, idx=None):
        item = self.getItem(key)
        if item is not None:
            if idx is not None:
                item.remove(idx)
            elif item.raw:
                if key in self.items:
                    del self.items[key]
                else:
                    for ii, item in self.items.items():
                        if item.name == key:
                            del self.items[ii]
                            break
            else:
                item.set(None)

    def flush(self):
        try:
            self._flush()
        except AttributeError:
            return False

        return True

    def _flush(self):
        def _count(options):
            ret = 0
            for opt in options:
                item = self.items.get(opt)
                if 'invisible' not in item.attr:
                    ret += 1

            return ret

        config = ConfigParser.ConfigParser()
        for section in self.template.values():
            options = section.get('options')
            if _count(options) == 0:
                continue

            sect = section.get('section')

            count = 0
            config.add_section(sect)
            for opt, option in options.items():
                item = self.getItem(opt)
                if 'invisible' in item.attr:
                    continue

                default = item.default(option, item.attr)
                # not to store the non-persist values
                vals = item.value if item.persist else item.tempValue
                if vals is None or vals == default:
                    continue

                if item.name.startswith(sect.lower()):
                    name = item.name[len(sect) + 1:]
                else:
                    name = item.name.split('/')[-1]

                if 'range' in item.attr:
                    for k, v in vals.items():
                        count += 1
                        config.set(sect, '%s_%s' % (name, str(k)), v
                                   if isinstance(v, unicode) else str(v))
                else:
                    count += 1
                    config.set(
                        sect, name, vals
                        if isinstance(vals, unicode) else str(vals))

            if count == 0:
                config.remove_section(sect)

        with utils.openfile(self.filename, 'w', 'utf-8') as fp:
            for section in config.sections():
                fp.write('[%s]\n' % section)
                for opt, v in config.items(section):
                    fp.write('%s=%s\n' % (opt, v))


class Config(Reader, Writer):
    def __init__(self, filename):
        Writer.__init__(self, filename, _CFG_PARA_TEMPLATE)
        Reader.__init__(self, filename, _CFG_PARA_TEMPLATE)

    def getTemplate(self):
        return self.template

    def getKeyType(self, key):
        item = self.getItem(key)
        if item is not None:
            return item.attr
        else:
            return ''

    def installPlxConfig(self, name, sname, opt):
        options = dict()
        for o in opt:
            options[o.get('name', '-')] = o

        template = {
            name: {
                'name': name,
                'section': '%s/%s' % (RUNTIME_PLUGIN_SECTION, sname),
                'options': options
            }
        }

        ci = self.doConvertToItems(template, True)

        options = dict()
        for section in template.values():
            for option in section.get('options').values():
                options[ci[option.get('name')].name] = option

        self.insertOption(RUNTIME_PLUGIN_SECTION, options)
        for ii, item in ci.items():
            if item.name in self.items:
                # update the item value if the attr is updated
                item.set(self.items[item.name].value)

            self.items[item.name] = item

    def getPlxConfigsWithName(self, name):
        ret = dict()

        prefix = ('%s/%s' % (RUNTIME_PLUGIN_SECTION, name)).lower()
        for item in self.items.values():
            if item.name.startswith(prefix):
                ret[item.orig[len(prefix) + 1:]] = item.value

        return ret
